package org.cloudfoundry.reconfiguration;
import java.io.IOException;
import java.io.StringReader;
import java.util.ArrayList;
import java.util.List;
import java.util.Map;
import java.util.Properties;
import java.util.logging.Level;
import java.util.logging.Logger;
import javax.sql.DataSource;
import org.cloudfoundry.runtime.env.CloudEnvironment;
import org.cloudfoundry.runtime.env.CloudServiceException;
import org.cloudfoundry.runtime.service.relational.MysqlServiceCreator;
import org.springframework.beans.BeansException;
import org.springframework.beans.MutablePropertyValues;
import org.springframework.beans.PropertyValue;
import org.springframework.beans.PropertyValues;
import org.springframework.beans.factory.BeanFactory;
import org.springframework.beans.factory.NoSuchBeanDefinitionException;
import org.springframework.beans.factory.config.BeanDefinition;
import org.springframework.beans.factory.config.BeanDefinitionHolder;
import org.springframework.beans.factory.config.BeanFactoryPostProcessor;
import org.springframework.beans.factory.config.BeanReference;
import org.springframework.beans.factory.config.ConfigurableListableBeanFactory;
import org.springframework.beans.factory.config.PropertiesFactoryBean;
import org.springframework.beans.factory.config.TypedStringValue;
import org.springframework.beans.factory.support.DefaultListableBeanFactory;
import org.springframework.beans.factory.support.ManagedProperties;
import org.springframework.core.Ordered;
import org.springframework.core.io.DefaultResourceLoader;
import org.springframework.core.io.Resource;
import org.springframework.core.io.support.PropertiesLoaderUtils;
/**
* A bean factory post processor that auto-stage service-related beans.
* <p>
* Currently, this bean supports auto-staging of {@link DataSource} beans.
*
* @author Ramnivas Laddad
*
*/
public class CloudAutoStagingBeanFactoryPostProcessor implements BeanFactoryPostProcessor, Ordered {
Logger logger = Logger.getLogger(CloudAutoStagingBeanFactoryPostProcessor.class.getName());
private CloudEnvironment cloudEnvironment;
private static final String CLOUDFOUNDRY_PROPERTIES = "META-INF/cloudfoundry.properties";
private static final String APP_CLOUD_DATA_SOURCE_NAME = "__appCloudDataSource";
private static final String APP_CLOUD_JPA_REPLACEMENT_PROPERTIES = "__appCloudJpaReplacementProperties";
private static final String APP_CLOUD_HIBERNATE_REPLACEMENT_PROPERTIES = "__appCloudHibernateReplacementProperties";
public void postProcessBeanFactory(ConfigurableListableBeanFactory beanFactory) throws BeansException {
if (autoStagingOff()) {
return;
}
DefaultListableBeanFactory defaultListableBeanFactory = (DefaultListableBeanFactory) beanFactory;
// defaultListableBeanFactory.getBean(CloudEnvironment.class) will do,
// but we go through a mechanism that will work for spring-2.5.x as well
@SuppressWarnings("unchecked")
Map<String,CloudEnvironment> cloudEnvironmentBeans = defaultListableBeanFactory.getBeansOfType(CloudEnvironment.class);
if (cloudEnvironmentBeans.size() > 1) {
logger.log(Level.INFO, "Multiple (" + cloudEnvironmentBeans.size() + ") CloudEnvironmentBeans found; zero or 1 expected");
return;
} else if (cloudEnvironmentBeans.size() == 1) {
cloudEnvironment = cloudEnvironmentBeans.entrySet().iterator().next().getValue();
} else {
cloudEnvironment = new CloudEnvironment();
}
if(processDatasources(defaultListableBeanFactory)) {
processJpaFactories(defaultListableBeanFactory);
processHibernateFactories(defaultListableBeanFactory);
}
}
private boolean autoStagingOff() {
return autoStagingOff(CLOUDFOUNDRY_PROPERTIES);
}
/**
* WARNING: Experimental support to opt out of autostaging, useful if autostaging comes in your way.
*
* Turn off autostaging if we find a META-INF/cloudfoundry.properties on classpath and it
* contains autostaging=false. Applications can opt-out of autostaging by adding a property
* to src/main/resources/META-INF/cloudfoundry.properties (assuming Maven layout).
*
* @return return true to opt out of autostaging
*/
boolean autoStagingOff(String propertyLocation) {
try {
Resource cloudfoundryConfig = new DefaultResourceLoader().getResource(propertyLocation);
if (!cloudfoundryConfig.exists()) {
logger.log(Level.INFO, "No 'META-INF/cloudfoundry.properties' found, autostaging is active");
return false;
}
Properties cloudfoundryProperties = PropertiesLoaderUtils.loadProperties(cloudfoundryConfig);
String autostagingStringValue = cloudfoundryProperties.getProperty("autostaging", "true");
boolean autostagingValue = Boolean.valueOf(autostagingStringValue);
if (!autostagingValue) {
logger.log(Level.INFO, "Application requested to skip autostaging");
}
return !autostagingValue;
} catch (Exception ex) {
// Turn off autostaging if anything goes wrong in our detection
return true;
}
}
// Let this be the last to process
@Override
public int getOrder() {
return LOWEST_PRECEDENCE;
}
private boolean processDatasources(DefaultListableBeanFactory defaultListableBeanFactory) {
String[] dataSourceBeanNames = getRealDataSources(defaultListableBeanFactory);
if (dataSourceBeanNames.length == 0) {
logger.log(Level.INFO, "No datasources found in application context");
return false;
} else if (dataSourceBeanNames.length > 1) {
logger.log(Level.INFO, "More than 1 (" + dataSourceBeanNames.length + ") real datasources found in application context. Skipping autostaging.");
return false;
}
DataSource theOnlyDataSource = null;
try {
MysqlServiceCreator mysqlCreationHelper = new MysqlServiceCreator(cloudEnvironment);
theOnlyDataSource = mysqlCreationHelper.createSingletonService().service;
} catch (CloudServiceException ex) {
logger.log(Level.INFO, "Multiple database services found. Skipping autostaging");
return false;
}
defaultListableBeanFactory.registerSingleton(APP_CLOUD_DATA_SOURCE_NAME, theOnlyDataSource);
for (String dataSourceBeanName : dataSourceBeanNames) {
if (dataSourceBeanName.equals(APP_CLOUD_DATA_SOURCE_NAME)) {
continue;
}
defaultListableBeanFactory.removeBeanDefinition(dataSourceBeanName);
defaultListableBeanFactory.registerAlias(APP_CLOUD_DATA_SOURCE_NAME, dataSourceBeanName);
}
return true;
}
private void processJpaFactories(DefaultListableBeanFactory beanFactory) {
processBeanProperties(beanFactory, "org.springframework.orm.jpa.AbstractEntityManagerFactoryBean",
APP_CLOUD_JPA_REPLACEMENT_PROPERTIES, "jpaProperties");
}
private void processHibernateFactories(DefaultListableBeanFactory beanFactory) {
processBeanProperties(beanFactory, "org.springframework.orm.hibernate3.AbstractSessionFactoryBean",
APP_CLOUD_HIBERNATE_REPLACEMENT_PROPERTIES, "hibernateProperties");
}
private void processBeanProperties(DefaultListableBeanFactory beanFactory,
String beanClassName, String replacementPropertiesName,
String propertyKey) {
Class<?> beanClass = loadClass(beanClassName);
if (beanClass == null) {
return;
}
try {
// TODO: Required in Grails case and need to reexamine
beanFactory.getBeanDefinition(replacementPropertiesName);
} catch (Exception ex){
return;
}
String[] beanNames = beanFactory.getBeanNamesForType(beanClass);
for (String beanName : beanNames) {
BeanDefinition beanDefinition = getBeanDefinition(beanFactory, beanName);
MutablePropertyValues propertyValues = beanDefinition.getPropertyValues();
PropertyValue originalProperties = propertyValues.getPropertyValue(propertyKey);
Properties originalPropertyValue = null;
if (originalProperties != null) {
Object value = originalProperties.getValue();
if (value instanceof Properties) {
originalPropertyValue = (Properties) originalProperties.getValue();
} else if (value instanceof BeanDefinitionHolder) {
originalPropertyValue = extractProperties((BeanDefinitionHolder) value);
} else if (value instanceof BeanReference) {
originalPropertyValue = extractProperties((BeanReference) value, beanFactory);
} else if (value instanceof TypedStringValue) {
originalPropertyValue = extractProperties((TypedStringValue) value);
} else {
throw new IllegalStateException("Unable to process property " + originalProperties.getName() + " of " + value.getClass() + " type");
}
} else {
originalPropertyValue = new ManagedProperties();
}
ManagedProperties replacementProperties
= loadReplacementPropertyValues(beanFactory, replacementPropertiesName);
replacementProperties.setMergeEnabled(true);
replacementProperties = (ManagedProperties) replacementProperties.merge(originalPropertyValue);
propertyValues.addPropertyValue(new PropertyValue(propertyKey, replacementProperties));
}
}
private Properties extractProperties(BeanDefinitionHolder beanDefinitionHolder) {
try {
BeanDefinition valBeanDefinition = beanDefinitionHolder.getBeanDefinition();
return getMapWrappingBeanProperties(valBeanDefinition);
} catch (Exception e) {
throw new IllegalStateException("Error processing property replacement for a BeanDefinitionHolder", e);
}
}
private Properties extractProperties(BeanReference beanReference, DefaultListableBeanFactory beanFactory) {
try {
BeanDefinition beanDefinition = getBeanDefinition(beanFactory,
beanReference.getBeanName());
return getMapWrappingBeanProperties(beanDefinition);
} catch (Exception e) {
throw new IllegalStateException("Error processing property replacement for a BeanDefinitionHolder", e);
}
}
private Properties extractProperties(TypedStringValue typeStringValue) {
Object value = typeStringValue.getValue();
if (value instanceof Properties) {
return (Properties)value;
} else if (value instanceof String) {
Properties props = new Properties();
try {
props.load(new StringReader((String)value));
return props;
} catch (IOException e) {
throw new IllegalStateException("Error processing property replacement for a TypedStringValue", e);
}
} else {
throw new IllegalStateException("Error processing property replacement for a TypedStringValue of value type " + value.getClass());
}
}
private ManagedProperties loadReplacementPropertyValues(DefaultListableBeanFactory beanFactory, String replacementPropertiesName) {
BeanDefinition replacementPropertiesBeanDef = beanFactory.getBeanDefinition(replacementPropertiesName);
return (ManagedProperties) replacementPropertiesBeanDef.getPropertyValues().getPropertyValue("properties").getValue();
}
private String[] getRealDataSources( DefaultListableBeanFactory beanFactory) {
String[] dataSourceBeanNames = beanFactory.getBeanNamesForType(DataSource.class);
Class<?> txAwareDSClass = loadClass("org.springframework.jdbc.datasource.TransactionAwareDataSourceProxy");
if (txAwareDSClass == null) {
return dataSourceBeanNames;
}
// In Scala, could have been one line and not even need contains()!
String[] txAwareDSBeanNames = beanFactory.getBeanNamesForType(txAwareDSClass);
List<String> realDSBeanNames = new ArrayList<String>();
for (String dataSourceBeanName : dataSourceBeanNames) {
if (!contains(txAwareDSBeanNames, dataSourceBeanName)) {
// Skip singletons
try {
if (getBeanDefinition(beanFactory, dataSourceBeanName) != null) {
realDSBeanNames.add(dataSourceBeanName);
}
} catch (NoSuchBeanDefinitionException ex) {
// skip
}
}
}
return realDSBeanNames.toArray(new String[0]);
}
private <T> boolean contains(T[] array, T searchElement) {
for (T element : array) {
if (element.equals(searchElement)) {
return true;
}
}
return false;
}
private BeanDefinition getBeanDefinition(DefaultListableBeanFactory beanFactory, String beanName) {
if (beanName.startsWith(BeanFactory.FACTORY_BEAN_PREFIX)) {
beanName = beanName.substring(BeanFactory.FACTORY_BEAN_PREFIX.length());
}
return beanFactory.getBeanDefinition(beanName);
}
private Properties getMapWrappingBeanProperties(BeanDefinition beanDefinition) {
if (beanDefinition.getBeanClassName().equals(PropertiesFactoryBean.class.getName())) {
try {
PropertyValues propertyValues = beanDefinition.getPropertyValues();
if (propertyValues.contains("location")) {
String propertyLocation = (String) propertyValues.getPropertyValue("location").getValue();
return PropertiesLoaderUtils.loadAllProperties(propertyLocation);
} else if (propertyValues.contains("properties")) {
return mapToProperties((Map<String,String>)propertyValues.getPropertyValue("properties").getValue());
} else {
throw new IllegalArgumentException("Unable to process PropertiesFactoryBean; doesn't contain either 'locations' or 'properties' property");
}
} catch (IOException ex) {
throw new IllegalArgumentException("Unable to process PropertiesFactoryBean", ex);
}
} else {
@SuppressWarnings("unchecked")
Map<String,String> sourceMap = (Map<String,String>) beanDefinition.getPropertyValues().getPropertyValue("sourceMap").getValue();
return mapToProperties(sourceMap);
}
}
private Properties mapToProperties(Map<String, String> map) {
Properties properties = new Properties();
for (Map.Entry<String, String> entry : map.entrySet()) {
properties.put(entry.getKey(), entry.getValue());
}
return properties;
}
private Class<?> loadClass(String name) {
try {
return Class.forName(name);
} catch (Throwable ex) {
return null;
}
}
}